﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace JESAI.Core.NetTools
{
    public delegate void OutputMethodHandler(string format, params object[] args);

    public class NetworkInfo
    {
        /// <summary>
        /// 获取本机IP地址
        /// </summary>
        public string GetLocalIp()
        {
            var addrs = GetLocalIpList();
            return addrs.First();
        }

        /// <summary>
        /// 获取本机IP地址
        /// </summary>
        public string GetIpByUri(string uri)
        {
            if (string.IsNullOrWhiteSpace(uri))
            {
                return null;
            }
            var uriSplits=uri.Split('\\');
            if (uriSplits.Length < 2)
            {
                return null;
            }

            uriSplits = uriSplits[1].Split(":"); //处理有端口的情况
           
            return uriSplits[0];
        }

        /// <summary>
        /// 获取本机IP地址列表
        /// </summary>
        public IList<string> GetLocalIpList()
        {
            string hostName = Dns.GetHostName();
            IPHostEntry ipEntry = Dns.GetHostEntry(hostName);
            //Dns.GetHostByName(myName);已过时
            IEnumerable<IPAddress> addrs = ipEntry.AddressList.Where(a => a.AddressFamily == AddressFamily.InterNetwork);
            return addrs.Select(a => a.ToString()).ToList();
        }

        /// <summary>
        /// 根据IP地址获得主机名称
        /// </summary>
        /// <param name="ip">主机的IP地址</param>
        /// <returns>主机名称</returns>
        public string GetHostNameByIp(string ip)
        {
            ip = ip.Trim();
            if (ip == string.Empty)
                return string.Empty;
            else if (ip == "127.0.0.1")
                return "localhost";

            string message;
            try
            {
                // 是否 Ping 的通
                if (Ping(ip, out message))
                {
                    IPHostEntry host = Dns.GetHostEntry(ip);
                    return host.HostName;
                }
                else
                    return string.Empty;
            }
            catch (Exception)
            {
                return string.Empty;
            }
        }

        /// <summary>
        /// 根据主机名（域名）获得主机的IP地址
        /// </summary>
        /// <param name="hostName">主机名或域名</param>
        /// <example> GetIPByDomain("www.google.com");</example>
        /// <returns>主机的IP地址</returns>
        public string GetIpByHostName(string hostName)
        {
            hostName = hostName.Trim();
            if (hostName == string.Empty)
                return string.Empty;
            else if (hostName == "localhost")
                return "127.0.0.1";

            try
            {
                IPHostEntry host = Dns.GetHostEntry(hostName);
                return host.AddressList.Where(a => a.AddressFamily == AddressFamily.InterNetwork).First().ToString();
            }
            catch (Exception)
            {
                return string.Empty;
            }
        }

        /// <summary>
        /// 是否能 Ping 通指定的主机
        /// </summary>
        /// <param name="dest">Host name or Ip address</param>
        /// <returns>true 通，false 不通</returns>
        public bool Ping(string dest, out string message)
        {
            IPAddress address;
            bool isIp = IPAddress.TryParse(dest, out address);
            Ping p = new Ping();
            PingOptions options = new PingOptions();
            options.DontFragment = true;
            string data = "01010101010101010101010101010101";  //32 bytes
            byte[] buffer = Encoding.ASCII.GetBytes(data);
            int timeout = 3000;
            var reply = p.Send(dest, timeout, buffer, options);

            if (!isIp && reply.Address != null)
                dest = reply.Address.ToString();

            if (reply.Status == IPStatus.Success)
            {
                var temp_time = reply.RoundtripTime < 1 ? "time<1ms" : $"time={reply.RoundtripTime}ms";
                var temp_ttl = reply.Options != null ? $" TTL={reply.Options.Ttl}" : "";
                message = $"Reply from {dest}: bytes={buffer.Length} {temp_time}{temp_ttl}";
                return true;
            }
            else
            {
                if (reply.Status == IPStatus.TimedOut || reply.Status == IPStatus.Unknown)
                    message = $"Request {DisplayPingStatus(reply.Status)}";
                else
                    message = $"Request from {dest}: {DisplayPingStatus(reply.Status)}";
                return false;
            }
        }

        /// <summary>
        /// PING 指定主机并输出结果
        /// </summary>
        /// <param name="dest">Host name or Ip address</param>
        public void PingOutput(string dest, OutputMethodHandler output)
        {
            IPAddress address;
            bool isIp = IPAddress.TryParse(dest, out address);

            Ping p = new Ping();
            PingOptions options = new PingOptions();
            options.DontFragment = true;
            string data = "01010101010101010101010101010101";
            byte[] buffer = Encoding.ASCII.GetBytes(data);  //32 bytes
            int timeout = 3000;
            int sent = 0, receive = 0;
            long roundTripTimeMin = 0, roundTripTimeMax = 0, roundTripTimeSum = 0;
            output("Pinging {0} with {1} bytes of data:", dest, buffer.Length);
            Thread.Sleep(300);
            try
            {
                for (var i = 0; i < 4; i++)
                {
                    sent++;
                    PingReply reply = p.Send(dest, timeout, buffer, options);

                    if (reply != null)
                    {
                        if (!isIp && reply.Address != null)
                            dest = reply.Address.ToString();

                        if (reply.Status == IPStatus.Success)
                        {
                            var temp_time = reply.RoundtripTime < 1 ? "time<1ms" : $"time={reply.RoundtripTime}ms";
                            var temp_ttl = reply.Options != null ? $" TTL={reply.Options.Ttl}" : "";
                            output("Reply from {0}: bytes={1} {2}{3}", dest, buffer.Length, temp_time, temp_ttl);
                            roundTripTimeSum += reply.RoundtripTime;
                            if (roundTripTimeMin == 0 || reply.RoundtripTime < roundTripTimeMin)
                                roundTripTimeMin = reply.RoundtripTime;
                            if (roundTripTimeMax == 0 || reply.RoundtripTime > roundTripTimeMax)
                                roundTripTimeMax = reply.RoundtripTime;
                            receive++;
                        }
                        else
                        {
                            if (reply.Status == IPStatus.TimedOut || reply.Status == IPStatus.Unknown)
                                output($"Request {DisplayPingStatus(reply.Status)}");
                            else
                                output("Request from {0}: {1}", dest, DisplayPingStatus(reply.Status));
                        }
                    }

                    Thread.Sleep(500);
                }
                output("");
                output("Ping statistics for {0}:", dest);
                output("\tPackets: Sent = {0}, Received = {1}, Lost = {2} ({3}% loss),", sent, receive, sent - receive, ((sent - receive) * 100) / sent);
                if (receive > 0)
                {
                    output("Approximate round trip times in milli-seconds:");
                    output("\tMinimum = {0}ms, Maximum = {1}ms, Average = {2}ms", roundTripTimeMin, roundTripTimeMax, roundTripTimeSum / receive);
                }
            }
            catch (PingException ex)
            {
                output(ex.GetBaseException().Message);
            }
        }        

        /// <summary>
        /// Telnet 指定主机端口并输出
        /// </summary>
        public async Task TelnetOutput(string host, int port, OutputMethodHandler output)
        {
            output("Verifying host '{0}' [port {1}]", host, port);
            string message = string.Empty;
            CancellationTokenSource cancellationSource = new CancellationTokenSource();
            TentacleSoftware.Telnet.TelnetClient telnetClient = null;
            try
            {
                telnetClient = new TentacleSoftware.Telnet.TelnetClient(host, port, TimeSpan.FromSeconds(3), cancellationSource.Token);
                TelnetCallback callback = new TelnetCallback(output);
                telnetClient.MessageReceived += callback.HandleMessageReceived;
                telnetClient.ConnectionClosed += callback.HandleConnectionClosed;
                await telnetClient.Connect();
                await telnetClient.Send("magic!");                
                output("Connected");
                ExecuteAfter(telnetClient.Disconnect, 7000); //7秒后关闭链接
            }
            catch (Exception ex)
            {
                message = ex.GetBaseException().Message;
                output(message);
                if (telnetClient != null) telnetClient.Disconnect();
            }
            cancellationSource.CancelAfter(30000);  //30秒后取消任务（如存在）
            //https://github.com/Spksh/TentacleSoftware.Telnet
        }

        protected class TelnetCallback
        {
            private bool _firstReceived = true;
            public OutputMethodHandler Output { get; set; }
            public TelnetCallback(OutputMethodHandler output)
            {
                this.Output = output;
            }
            internal void HandleConnectionClosed(object sender, EventArgs e)
            {
                //Output("Discounted");
            }
            internal void HandleMessageReceived(object sender, string message)
            {
                if (_firstReceived) Output(string.Empty);
                Output(message);
                _firstReceived = false;
            }            
        }

        /// <summary>
        /// Socket连接端口
        /// </summary>
        /// <param name="dest">Host name or Ip address</param>
        public bool SocketConnect(string dest, int port, out string message)
        {
            IPAddress ip;
            bool destIsIpAddress = IPAddress.TryParse(dest, out ip);
            Socket sock = null;
            try
            {   
                using (sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
                {
                    sock.SendTimeout = 6000;
                    sock.ReceiveTimeout = 6000;
                    if (destIsIpAddress)
                    {
                        IPEndPoint point = new IPEndPoint(ip, port);
                        sock.Connect(point);
                    }
                    else
                    {
                        if (dest.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
                            dest = dest.Substring(7);
                        else if (dest.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
                            dest = dest.Substring(8);

                        sock.Connect(dest, port);
                    }

                    message = "Port opened [OK]";
                    return true;
                }
            }
            catch (SocketException ex)
            {
                message = ex.Message;
                return false;
            }
            finally
            {
                if (sock != null) sock.Close();
            }
        }

        /// <summary>
        /// HTTP GET 请求
        /// </summary>
        public bool HttpGet(string url, out string message)
        {
            HttpWebRequest req = WebRequest.CreateHttp(url);
            req.Headers.Add("Request-By", "AisinGioro.Server");
            req.AllowAutoRedirect = false;
            req.Timeout = 20000;
            Stream stream = null;

            if (url.StartsWith("https", StringComparison.OrdinalIgnoreCase))
                ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls;

            try
            {
                var sw = Stopwatch.StartNew();
                {
                    HttpWebResponse response = (HttpWebResponse)req.GetResponse();
                    stream = response.GetResponseStream();
                    sw.Stop();
                    message = $"{response.StatusDescription}/{(int)response.StatusCode} in {sw.ElapsedMilliseconds}ms";
                    return true;
                }
            }
            catch (Exception ex)
            {
                message = ex.Message;
                return false;
            }
            finally
            {
                if (stream != null)
                    stream.Close();
            }

            /*
            try
            {
                using (var hc = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false }))
                {
                    hc.MaxResponseContentBufferSize = 10 * 1024 * 1024; //10MB
                    hc.DefaultRequestHeaders.Add("user-agent", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36");
                    hc.DefaultRequestHeaders.Add("request-by", "AisinGioro");
                    hc.Timeout = new TimeSpan(0, 0, 20); //20 seconds
                    //hc.DefaultRequestHeaders.Connection.Add("keep-alive");

                    if (url.StartsWith("https", StringComparison.OrdinalIgnoreCase))
                        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls;

                    var sw = System.Diagnostics.Stopwatch.StartNew();
                    {
                        var response = hc.GetAsync(url).GetAwaiter().GetResult();
                        sw.Stop();
                        message = $"{response.ReasonPhrase}/{(int)response.StatusCode} in {sw.ElapsedMilliseconds}ms";
                        return response.IsSuccessStatusCode;
                    }
                }
            }
            catch (Exception ex)
            {
                message = ex.GetBaseException().Message;
                return false;
            }
            */
        }

        /// <summary>
        /// 指定时间后执行操作
        /// </summary>
        private void ExecuteAfter(Action action, int delay)
        {
            Task.Factory.StartNew(() =>
            {
                Thread.Sleep(delay);
                action();
            });
        }

        private string DisplayPingStatus(IPStatus status)
        {
            if (status == IPStatus.Unknown) return "Unknown";
            else if (status == IPStatus.Success) return "Success";
            else if (status == IPStatus.DestinationNetworkUnreachable) return "Destination network unreachable";
            else if (status == IPStatus.DestinationHostUnreachable) return "Destination host unreachable";
            else if (status == IPStatus.DestinationProtocolUnreachable) return "Destination protocol unreachable";
            else if (status == IPStatus.DestinationProhibited) return "Destination prohibited";
            else if (status == IPStatus.DestinationPortUnreachable) return "Destination port unreachable";
            else if (status == IPStatus.NoResources) return "No resources";
            else if (status == IPStatus.BadOption) return "Bad option";
            else if (status == IPStatus.HardwareError) return "Hardware error";
            else if (status == IPStatus.PacketTooBig) return "Packet too big";
            else if (status == IPStatus.TimedOut) return "Timedout";
            else if (status == IPStatus.BadRoute) return "Bad route";
            else if (status == IPStatus.TtlExpired) return "TTL expired";
            else if (status == IPStatus.TtlReassemblyTimeExceeded) return "TTL reassembly time exceeded";
            else if (status == IPStatus.ParameterProblem) return "Parameter problem";
            else if (status == IPStatus.SourceQuench) return "Source quench";
            else if (status == IPStatus.BadDestination) return "Bad eestination";
            else if (status == IPStatus.DestinationUnreachable) return "Destination unreachable";
            else if (status == IPStatus.TimeExceeded) return "Time exceeded";
            else if (status == IPStatus.BadHeader) return "Bad header";
            else if (status == IPStatus.UnrecognizedNextHeader) return "Unrecognized next header";
            else if (status == IPStatus.IcmpError) return "ICMP error";
            else if (status == IPStatus.DestinationScopeMismatch) return "Destination scope mismatch";
            else return "Unknown";
        }
    }
}
